/* =============================================================
 * Ketchup4j : Squeeze the speed out of your objects. 
 * =============================================================
 *
 * (C) Copyright 2007, by Saverio Miroddi
 *
 * Project Info:  http://tinyhorrorsql.sourceforge.net/
 *
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by 
 * the Free Software Foundation; either version 3 of the License, or (at 
 * your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
 * USA.  
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
 * in the United States and other countries.]
 *
 * ---------------------------
 * MemoryCounter.java
 * ---------------------------
 * Author: Saverio Miroddi
 * Author: Heinz Max Kabutz
 * 
 */
package org.itmozart.ketchup4j.other;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map;

/**
 * This class is a slightly modified version of Kabutz's memory counter
 * presented in JavaSpecialists #142.<br>
 * This method has been chosen over JVM 5's Instrumentation because it doesn't
 * require some hoodoo-voodoo to run (no need to use JVM parameters and JAR
 * headers), and we don't need byte-precision for objects.
 * <p>
 * Choices:<ul>
 * <li>Static fields aren't evaluated.</li>
 * <li>Flyweigth (boxed) objects aren't evaluated.</li>
 * </ul>
 */
public final class MemoryCounter {
	private static Map<Object, Object> visited;
	
	static {
		visited = new IdentityHashMap<Object, Object>();
	}

	public static synchronized int estimate(Object obj) {
		int result = recursEstimate(obj, visited);
		visited.clear();
		return result;
	}

	private static int recursEstimate(Object object, Map<Object, Object> visited) {
		if (skipObject(object)) return 0;

		visited.put(object, null);
		
		Class<?> clazz = object.getClass();
		if (clazz.isArray()) return estimateArray(object, visited);

		// includes pointers; will be aligned
		int primitivesSz = 0;
		
		// size of pointed objects, the alignment is computed inside the
		// recursed function
		int referencedSz = 0;
		
		while (clazz != null) {
			Field[] fields = clazz.getDeclaredFields();
			for (Field field: fields) {
				if (! Modifier.isStatic(field.getModifiers())) {
					Class<?> fieldType = field.getType();
					if (fieldType.isPrimitive()) {
						primitivesSz += primitiveSizes.get(fieldType);
					}
					else {
						primitivesSz += SIZE_POINTER;
						field.setAccessible(true);
						try {
							Object pointedObject = field.get(object);
							referencedSz += recursEstimate(pointedObject, visited);
						}
						catch (IllegalAccessException ex) {
							throw new RuntimeException(ex);
						}
					}
				}
			}
			clazz = clazz.getSuperclass();
		}
		
		primitivesSz += SIZE_CLASS;		
		primitivesSz += primitivesSz % 8; // round on 8 bytes paragraph
		
		return primitivesSz + referencedSz; 
	}

	/**
	 * Tests null pointers and flyweight objects.
	 */
	private static boolean skipObject(Object obj)
	{
		if (obj instanceof Comparable) {
			if (obj instanceof Enum) {
				return true;
			}
			else if (obj instanceof String) {
				return (obj == ((String) obj).intern());
			}
			else if (obj instanceof Boolean) {
				return (obj == Boolean.TRUE || obj == Boolean.FALSE);
			}
			else if (obj instanceof Integer) {
				return (obj == Integer.valueOf((Integer) obj));
			}
			else if (obj instanceof Short) {
				return (obj == Short.valueOf((Short) obj));
			}
			else if (obj instanceof Byte) {
				return (obj == Byte.valueOf((Byte) obj));
			}
			else if (obj instanceof Long) {
				return (obj == Long.valueOf((Long) obj));
			}
			else if (obj instanceof Character) {
				return (obj == Character.valueOf((Character) obj));
			}
		}
		return (obj == null);
	}
	  
	protected static int estimateArray(Object array, Map<Object, Object> visited) {
		int result = SIZE_BASE_ARRAY;

		Class<?> arrayElementClazz = array.getClass().getComponentType();
		if (arrayElementClazz.isPrimitive()) {
			int length = Array.getLength(array);
			int primitiveSize = primitiveSizes.get(arrayElementClazz);			
			result += length * primitiveSize;
		} 
		else {			
			for (Object obj: (Object[])array) {
				result += SIZE_POINTER;
				result += recursEstimate(obj, visited);
			}
		}
		
		return result;
	}
	
	////////////////////////////////////////////////////////////////////////////
	// PRIMITIVE SIZES DATA
	////////////////////////////////////////////////////////////////////////////

	private static final Map<Class<?>, Integer> primitiveSizes = 
		new IdentityHashMap<Class<?>, Integer>() {
			{
				put(boolean.class, 1);
				put(byte.class, 1);
				put(char.class, 2);
				put(short.class, 2);
				put(int.class, 4);
				put(float.class, 4);
				put(double.class, 8);
				put(long.class, 8);
			}
		};
	
	private static final int SIZE_POINTER = 4;
	private static final int SIZE_CLASS = 8;
	private static final int SIZE_BASE_ARRAY = 16;
}